package com.pinfly.common.http;

import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;

import org.apache.log4j.Logger;

import com.pinfly.common.util.SafeBufferedReader;

/**
 * Class to send an HTTP request to a site and retrieve the response.
 * 
 * This class enables the user to specify an HTTP server, a file on that server
 * and optionally the body of a request. The class will then send the request to
 * the server and return the result. If a response is not received in a timely
 * fashion, then another attempt is made. Both the time between attempts and the
 * number of attempts can be specified.
 */
public class HttpRequest
{
    private static final Logger logger = Logger.getLogger (HttpRequest.class);

    private final static int DEFAULT_TIMEOUT = 1000; // Default time value in
                                                     // msec
    private final static int DEFAULT_RETRY_COUNT = 5; // Default number of
                                                      // retries

    private String m_host; // The host to send the request to
    private String m_file; // The file to request on that host
    private String m_content = null; // The body of the request
    private boolean m_haveResponse; // Have we received a response?
    private Object m_response; // The received response
    private int m_retryCount; // How many retries to do
    private int m_retries; // How many retries have been done
    private int m_timeout; // Timeout value between retries
    private Thread m_responseThread; // Thread that tries to get response
    private Thread m_timerThread; // Thread that sets timer
    private boolean m_suspended; // Are we waiting for a response?
    private Object m_suspSync = new Object (); // To synchronize access to
                                               // m_suspended

    /**
     * Construct an HttpRequest with no body content.
     * 
     * @param host A string specifying the host name
     * @param file A string specifying the file name. This may include a
     *            parameter string (e.g. "someFile?p1=v1&p2=v2")
     */
    public HttpRequest (String host, String file)
    {
        m_host = host;
        m_file = file;
    }

    /**
     * Construct an HttpRequest with no body content from combined hostFile
     * string.
     * 
     * @param hostFile A string specifying the host name / file name This may
     *            include a parameter string (e.g. "someFile?p1=v1&p2=v2")
     */
    public HttpRequest (String hostFile)
    {
        int n = hostFile.indexOf ('/');
        if (n != -1)
        {
            // separate the host from the file part
            m_file = hostFile.substring (n + 1);
            m_host = hostFile.substring (0, n);
        }
        else
        {
            // no file part just set the host
            m_host = hostFile;
            m_file = "";
        }
    }

    /**
     * Construct an HttpRequest that has some body content.
     * 
     * @param host A string specifying the host name
     * @param file A string specifying the file name. This may include a
     *            parameter string (e.g. "someFile?p1=v1&p2=v2")
     * @param content A string specifying the content of the request
     */
    public HttpRequest (String host, String file, String content)
    {
        m_host = host;
        m_file = file;
        m_content = content;
    }

    /**
     * Set the body content of a request.
     * 
     * @param content A string specifying the content of the request
     */
    public void setContent (String content)
    {
        m_content = content;
    }

    /**
     * Send the request and get the response.
     * 
     * Default values are used for the timeout value and retry count.
     * 
     * @return A string containing the body of the response
     * 
     * @throws MSHttpException If an error is encountered
     */
    public String getResponse () throws MSHttpException
    {
        return getResponse (DEFAULT_TIMEOUT, DEFAULT_RETRY_COUNT);
    }

    /**
     * Send the request and get the response.
     * 
     * The time to wait for a response and the number of retries may be
     * specified.
     * 
     * @param timeout The length of time to wait for a response, in msec
     * @param retryCount The number of times to retry if a response isn't
     *            received.
     * 
     * @return A string containing the body of the response
     * 
     * @throws MSHttpException If an error is encountered
     */
    public synchronized String getResponse (int timeout, int retryCount) throws MSHttpException
    {
        /*
         * This method does the real work of trying to get a response. It starts
         * a couple of new threads. One calls tryResponse() to actually get the
         * response; the other thread just sets a timer and waits.
         * 
         * When tryResponse() returns, its thread calls gotResponse(). The
         * gotResponse() method kills the timer thread, sets m_haveResponse to
         * true and puts the response itself in m_response. It then clears
         * m_suspended and wakes this method up.
         * 
         * If timer thread wakes up first, it calls timerDone(). Timer done
         * kills the tryResponse thread. If we have retried the specified number
         * of times, then timerDone leaves m_haveResponse set to false, clears
         * m_suspended, and wakes this method up.
         * 
         * If the specified number of retries have not been done, then
         * timerDone() repeats the process - it creates another thread to run
         * tryResponse() and another timer thread.
         * 
         * So, m_suspended will remain set until either responsedDone() is
         * called, in which case we got a response, or until timerDone() is
         * called and we have done the specified number of retries. We can
         * distinguish those two cases by examining the value of m_suspended.
         */
        m_suspended = true;
        m_haveResponse = false;
        m_retries = 0;
        m_retryCount = retryCount;
        m_timeout = timeout;
        createThreads ();

        while (m_suspended)
        {
            try
            {
                wait ();
            }
            catch (InterruptedException ex)
            {
            }
        }

        if (!m_haveResponse)
        {
            throw new MSHttpNoResponseException (getURLString ());
        }
        if (m_response instanceof MSHttpException)
        {
            throw (MSHttpException) m_response;
        }
        return (String) m_response;
    }

    private String tryResponse () throws MSHttpException
    {
        // This method actually makes the http connection, sends the request
        // and gets the response.
        // String urlString = "http://" + m_host + "/" + m_file;
        HttpURLConnection conn = null; // The actual connection
        PrintWriter out = null; // For writing the request body
        SafeBufferedReader in = null; // For reading the response
        StringBuffer response = new StringBuffer (); // For building the
                                                     // response

        // try block for creating and using connection
        try
        {
            // Create the URL and open the connection
            String u = getURLString ();
            URL url = new URL (u);
            logger.trace ("Opening connection to " + u);
            conn = (HttpURLConnection) url.openConnection ();

            // try block to send request
            try
            {
                // If there is a request body, send it down the connection
                if (m_content != null)
                {
                    conn.setRequestMethod ("POST");
                    conn.setDoOutput (true);
                    out = new PrintWriter (conn.getOutputStream ());
                    out.write (m_content);
                }
            }
            catch (IOException ex)
            {
                String msg = "I/O Exception while writing request";
                logger.warn (msg);
                throw new MSHttpException (ex, msg);
            }
            finally
            {
                if (out != null)
                    out.close ();
            }
            // end of try/catch/finally for sending request

            // try block to get the response
            try
            {
                // check the status to make sure there is a response
                int responseCode = conn.getResponseCode ();
                if (responseCode != HttpURLConnection.HTTP_OK)
                {
                    String msg = "HTTP Request returned status = " + responseCode + conn.getResponseMessage ();
                    logger.warn (msg);
                    throw new MSHttpException (msg);
                }

                // read the response
                in = new SafeBufferedReader (new InputStreamReader (conn.getInputStream ()));
                String line;
                boolean responding = false;

                while ((line = in.readLine ()) != null)
                {
                    if (!responding && line.length () == 0)
                        continue;

                    responding = true;
                    response.append (line);
                    response.append ("\n");
                }
            }
            catch (IOException ex)
            {
                String msg = "I/O Exception while reading response";
                logger.warn (msg, ex);
                throw new MSHttpException (ex, msg);
            }
            finally
            {
                try
                {
                    if (in != null)
                        in.close ();
                }
                catch (IOException ex)
                {
                }
            }
            // end of try/catch/finally for reading response

        }
        catch (MalformedURLException ex)
        {
            String msg = "Creating URL for " + getURLString ();
            logger.warn (msg, ex);
            throw new MSHttpException (ex, msg);
        }
        catch (IOException ex)
        {
            String msg = "I/O Exception creating connection";
            logger.warn (msg, ex);
            throw new MSHttpException (ex, msg);
        }
        finally
        {
            if (conn != null)
                conn.disconnect ();
        }
        // end of try/catch/finally for creating and using connection

        return response.toString ();
    }

    private String getURLString ()
    {
        return "http://" + m_host + "/" + m_file;
    }

    private void createThreads ()
    {
        /*
         * Create two threads, one to try and get the response, one to wait for
         * a timer to go off. See getResponse() for a detailed description of
         * how this works.
         */
        m_responseThread = new ResponseThread ();
        m_responseThread.start ();
        m_timerThread = new TimerThread ();
        m_timerThread.start ();
    }

    private synchronized void gotResponse (Object result)
    {
        /*
         * Got a result. See getResult() for a description of what this does.
         */

        // Stop both threads. Need to stop the response thread in case
        // the timer went off at the same time we got a response and it has
        // started another one
        m_timerThread.interrupt ();
        m_responseThread.interrupt ();

        // Save the result
        m_haveResponse = true;
        m_response = result;

        // Indicate that we are finished
        m_suspended = false;
        this.notify ();

        if (logger.isTraceEnabled ())
        {
            logger.trace ("Got " + ((result instanceof String) ? "result" : "exception") + " from " + getURLString ());
        }

    }

    private synchronized void timerDone ()
    {
        /*
         * The timer expired before we got a result. If have more retries, then
         * start everything again, else quit without a result. See getResult()
         * for more information on how this works
         */

        // Stop the request thread
        m_responseThread.interrupt ();

        if (++m_retries >= m_retryCount)
        {
            // Tried as many times as we are going to. So put a stop to
            // this without a result
            m_suspended = false;
            this.notify ();
            logger.trace ("Giving up on " + getURLString ());
        }
        else
        {
            // If we don't have a result, try again
            // But don't restart if we are getting a result right now
            if (m_suspended)
            {
                createThreads ();
                logger.trace ("Retrying on " + getURLString ());
            }
        }
    }

    private class ResponseThread extends Thread
    {
        /*
         * Thread that tries to get a response to the request. See
         * HttpRequest.getResponse() for a description of how this thread is
         * used.
         */
        public void run ()
        {
            String response = null;
            try
            {
                response = tryResponse ();
                // Got a real response
                gotResponse (response);
            }
            catch (MSHttpException ex)
            {
                // Got an exception, that's a response too
                gotResponse (ex);
            }
        }
    } // end of inner class ResponseThread

    private class TimerThread extends Thread
    {
        /*
         * Thread that monitors a timer. See HttpRequest.getResponse() for a
         * description of how this thread is used.
         */
        public void run ()
        {
            try
            {
                sleep (m_timeout);
                // We woke up without getting interrupted
                timerDone ();
            }
            catch (InterruptedException ex)
            {
                // We were interrupted, means we got a result.
            }
        }
    } // end of inner class TimerThread

}
